#!/usr/bin/env python3
# Copyright (C) 2024 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""
Runs a bisection on the autopush ui.perfetto.dev builds

Similar to git bisect, but bisects UI releases rather than commits.
This works only for autopush builds from main, ignores canary and stable
channels, as they make the history non-linear.

How it works:
- It first obtains an unordered list of versions from gs://ui.perfetto.dev
- Then obtains the list of ordered commits from git
- Intersects the two lists, keeping only git commits that have a corresponding
  ui autopush release.
- Proceeds with a guided bisect in the range.
"""

import argparse
import sys

from subprocess import check_output

COMMIT_ABBR_LEN = 9  # UI truncates commitish to 9 chars, e.g. v45.0-38b7c2b12.


def main():
  parser = argparse.ArgumentParser()
  parser.add_argument(
      '--good',
      default=None,
      help='Last good release (e.g. v44.0-257a02990).' +
      'Defaults to the first verion available')
  parser.add_argument(
      '--bad',
      default=None,
      help='First bad release. Defaults to the latest version available')
  args = parser.parse_args()

  print('Fetching list of UI releases from GCS...')
  rev_list = check_output(['gsutil.py', 'ls', 'gs://ui.perfetto.dev/']).decode()
  ui_map = {}  # maps '38b7c2b12' -> 'v45.0-38b7c2b12'
  for line in rev_list.split():
    version = line.split('/')[3]
    if '-' not in version:
      continue
    ver_hash = version.split('-')[1]
    ui_map[ver_hash] = version
  print('Found %d UI versions' % len(ui_map))

  # Get the linear history of all commits.
  print('Fetching revision history from git...')
  ui_versions = []
  git_out = check_output(['git', 'rev-list', '--left-only',
                          'origin/main']).decode()
  for line in git_out.split():
    line = line.strip()
    rev = line[0:COMMIT_ABBR_LEN]
    if rev not in ui_map:
      continue  # Not all perfetto commits have a UI autopush build.
    ui_versions.append(ui_map[rev])

  # git rev-list emits entries in recent -> older versions. Reverse it.
  ui_versions.reverse()

  # Note that not all the entries in ui_map will be present in ui_versions.
  # This is because ui_map contains also builds coming from canary and stable
  # branches, that we ignore here.

  start = ui_versions.index(args.good) if args.good else 0
  end = ui_versions.index(args.bad) if args.bad else len(ui_versions) - 1
  while end - start > 1:
    print('\033c', end='')  # clear terminal.
    print(
        'Bisecting from %s (last good) to %s (first bad), %d revisions to go' %
        (ui_versions[start], ui_versions[end], end - start + 1))
    mid = (end + start) // 2

    # Print a visual indication of where we are in the bisect.
    for i in reversed(range(start, end + 1)):
      sfx = ''
      if i == start:
        sfx = ' GOOD --------------'
      elif i == end:
        sfx = ' BAD ---------------'
      elif i == mid:
        sfx = ' <- version to test'
      print(ui_versions[i] + sfx)

    user_feedback = input(
        'https://ui.perfetto.dev/%s/. Type g for good and b for bad: ' %
        ui_versions[mid])
    if user_feedback == 'b':
      end = mid
    elif user_feedback == 'g':
      start = mid
    else:
      print('Unrecognised key "%d", try again' % user_feedback)

  print('First bad UI release %s' % ui_versions[end])
  print('You should now inspect the individual commits via git log good..bad')


if __name__ == '__main__':
  sys.exit(main())
